﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using DependencyAnalysis.Algorithms.CycleDetection;
using DependencyAnalysis.DataModel;
using DependencyAnalysis.Internal;

namespace DependencyAnalysis
{
    public static class Principles
    {
        /// <summary>
        /// The dependency graph of packages (modules and submodules) must have no cycles. Cycles increase the work to re-build
        /// and eventually make every package depend on every other package.
        /// Return strongly connected subgraphs in the dependency graph of submodules. Those describe cycles.
        /// Cyclic dependencies on module level are already covered by your compiler.
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [Pure]
        public static IList<IList<SubmoduleDescriptor>> AcyclicDependencies(IDependencyModel model)
        {
            var nodes = new Dictionary<SubmoduleDescriptor, DependencyNode>();
            foreach (SubmoduleDescriptor descriptor in model.DependenciesPerSubmoduleOut.Keys)
            {
                var node = new DependencyNode { Tag = descriptor };
                nodes.Add(descriptor, node);
            }
            foreach (var pair in model.DependenciesPerSubmoduleOut)
            {
                var submoduleDescriptor = (SubmoduleDescriptor)pair.Key;
                var node = nodes[submoduleDescriptor];

                foreach (var reference in pair.Value)
                {
                    var submodule = reference.Target.ImplementingType.Submodule;
                    if (!nodes.ContainsKey(submodule)) continue;
                    var dependency = nodes[submodule];
                    if (node.Dependencies.Contains(dependency)) continue;
                    node.Dependencies.Add(dependency);
                }
            }
            var cycleDetection = new TarjanCycleDetection();
            var cycles = cycleDetection.ReportCycles(nodes.Values.ToList());
            return cycles.Select(cycle => cycle.Select(node => (SubmoduleDescriptor)node.Tag).ToList()).Cast<IList<SubmoduleDescriptor>>().ToList();
        }

        /// <summary>
        /// The dependencies between packages should be in the direction of the stability of the packages.
        /// A package should only depend upon packages that are more stable than it is. Stability describes
        /// the likelihood for a package of not changing.
        /// Find any dependencies between modules or between submodules, where the target of the dependenc is less
        /// stable than the source.
        /// </summary>
        /// <typeparam name="T">either ModuleDescriptor or SubmoduleDescriptor</typeparam>
        /// <param name="model"></param>
        /// <returns></returns>
        [Pure]
        public static IList<Tuple<T, double, T, double>> StableDependencies<T>(IDependencyModel model) where T : IDescriptor
        {
            if (typeof(T) != typeof(ModuleDescriptor) && typeof(T) != typeof(SubmoduleDescriptor))
            {
                throw new ArgumentException("generic parameter T must be ModuleDescriptor or SubmoduleDescriptor");
            }
            var metrics = new MetricsImplementation(model);
            var instabilityMap = new Dictionary<IDescriptor, double>();
            var dependencyStructure = DependencyModelConverter.GetDependencyStructureOut<T>(model);
            foreach (var descriptor in dependencyStructure.Keys)
            {
                instabilityMap.Add(descriptor,
                    typeof(T) == typeof(ModuleDescriptor) ? metrics.Instability((ModuleDescriptor)descriptor) : metrics.Instability((SubmoduleDescriptor)descriptor));
            }
            var instableDependencies = new List<Tuple<T, double, T, double>>();
            foreach (var dependencies in dependencyStructure)
            {
                double instabilitySource = instabilityMap[dependencies.Key];
                IEnumerable<IDescriptor> references;
                if (typeof(T) == typeof(ModuleDescriptor))
                {
                    references = VerifierUtilities.AggregateDependenciesToModules(dependencies.Value, false);
                }
                else
                {
                    references = VerifierUtilities.AggregateDependenciesToSubmodules(dependencies.Value, false);
                }
                foreach (T reference in references)
                {
                    double instabilityTarget = instabilityMap[reference];
                    if (instabilitySource < instabilityTarget)
                    {
                        instableDependencies.Add(new Tuple<T, double, T, double>((T)dependencies.Key, instabilitySource, reference, instabilityTarget));
                    }
                }
            }
            return instableDependencies;
        }

        /// <summary>
        /// Repeated code unnecessarily inflates the code base. It may also hide the real purpose of a method since it
        /// then fulfills multiple tasks. Even worse is the impact on maintainability. Changes to one occurence are not
        /// transferred to all other places. The code base ends up with slightly diverging implementations of the
        /// same logic.
        /// Duplicated code inside the same class or in sibling classes can often be solved by extracting a method.
        /// Duplication in unrelated classes is candidate for the extract class refactoring.
        /// The method determines possible duplications by comparing the instructions of each method. There are usually
        /// many more instructions than lines of code. A good value for minimalLength to query is about 40.
        /// </summary>
        /// <param name="model"></param>
        /// <param name="minimalLength">minimal number of instructions to qualify for a duplicate, take Maximum(10, minimalLength)</param>
        [Pure]
        public static IList<CodeDuplicate> DontRepeatYourself(IDependencyModel model, int minimalLength)
        {
            minimalLength = Math.Max(10, minimalLength);
            InstructionCache cache = model.InstructionCache;
            var modules = model.DependenciesPerModule.Keys.Where(x => ((ModuleDescriptor)x).IsPartOfProject).ToList();
            var memberHash = new HashSet<MemberDescriptor>();
            modules.ForEach(x => VerifierUtilities.SelectMembers(model, x, memberHash));

            var memberInstructions = DontRepeatYourselfPrinciple.SelectRelevantMembers(minimalLength, memberHash, cache);

            var duplicates = DontRepeatYourselfPrinciple.FindDuplicatesBetweenMembers(minimalLength, memberInstructions);
            var duplicatesInside = DontRepeatYourselfPrinciple.FindDuplicatesInsideMembers(minimalLength, memberInstructions);
            duplicates.AddRange(duplicatesInside);
            return duplicates;
        }

        /// <summary>
        /// The law of Demeter limits the interaction between classes in a system. It supports
        /// loose coupling of components since access to distant classes is prohibited.
        /// A method of an object is allowed to invoke the following kinds of objects:
        /// - members the current class
        /// - members of its parameters
        /// - methods of associated classes (e.g. the return value of a method of the current class)
        /// - global variables (all accessible static members)
        /// 
        /// In general, call chains like "object.Something().Foo().Bar()" are forbidden. However, it is
        /// allowed to access members of data objects in a chain as long as the chain starts with a data
        /// object.
        /// 
        /// Builder pattern is allowed, too.
        /// 
        /// Allowed:
        /// - dataObject.MemberOfDO.AnotherMemberOfDO...
        /// - classObject.Member (stop)
        /// - MemberOfCurrentObject().AnotherMemberOfCurrent()...
        /// - builder.Build(x).Build(y).Build(z)...
        /// 
        /// Forbidden:
        /// - classObject.Member.Member
        /// - classObject.MemberReturnsDO().MemberOfDO
        /// - classObject.Member(parameters).AnotherMember(parameters)
        /// </summary>
        /// <param name="model"></param>
        /// <returns>by member return first chance for breaking the law</returns>
        [Pure]
        public static IDictionary<MemberDescriptor, IEnumerable<MemberDescriptor>> LawOfDemeter(IDependencyModel model)
        {
            var instructionCache = model.InstructionCache;
            var members = (from x in model.DependenciesPerMember.Keys
                           where !((MemberDescriptor)x).IsGenerated
                           select x).ToList();
            var violations = LawOfDemeterPrinciple.SearchForConcatenatedCalls(members, instructionCache);
            return violations;
        }

        /// <summary>
        /// When the type Tchild is derived from Tparent, then any behavioral assumption that can be made about an object of the mutable type Tparent must also hold for objects of the derived type Tchild. Otherwise these types cannot be exchanged without altering the context they live in.
        /// DependencyAnalysis implements a validation for LSP of members and types. For types so far, the ruleset is pretty simple. The rules might be extended over time:
        /// - Rules for variance are already covered by the compiler, so they are not explicitely checked 
        /// - At least one overridden member violates the LSP  
        /// - Constructors count as overrides 

        /// An overridden member obviously violates the Liskov Substitution Principle when one of the following applies:
        /// - The member does not call the base member, given that it is concrete rather than interface or abstract.  
        /// - The method returns before calling the concrete base member. 
        /// - It throws an exception type not assignable to exceptions from base member of any inherited type.  
        /// - There is a conditional branch on input parameters with return or throw statement  
        /// - The member throws an exception before calling the concrete base member. 
        /// - Fields declared in base types are modified. Modifications to state of the current type are allowed though, as they are extensions not modifications of the supertype.  
        /// - The member calls to a state changing method of a base type.  
        /// - The return value of a method is modified, properties of reference types are allowed to be touched though
        /// </summary>
        /// <param name="model"></param>
        /// <returns>by type return any member that breaks with LSP</returns>
        [Pure]
        public static IDictionary<TypeDescriptor, IEnumerable<MemberDescriptor>> LiskovSubstitution(IDependencyModel model)
        {
            if (model == null)
            {
                throw new ArgumentNullException("model");
            }
            var liskovSubstitutionPrinciple = new LiskovSubstitutionPrinciple(model);
            var violations = new Dictionary<TypeDescriptor, IEnumerable<MemberDescriptor>>();

            foreach (var pair in model.DependenciesPerTypeOut)
            {
                var typeDescriptor = (TypeDescriptor)pair.Key;
                violations.Add(typeDescriptor, liskovSubstitutionPrinciple.SelectViolatingMembers(typeDescriptor.Members));
            }

            return violations;
        }

        /// <summary>
        /// Definition
        /// Methodenreinheit is term from mathematics, which describes the preference for solutions that do not introduce foreign concepts to solve a problem. For a programmers solution a foreign concept is the change of something outside the method. A method is considered pure, when its execution has no recognizable side effects on its environment.
        /// When should a method be implemented pure?
        /// Whenever reproducibility is a requirement for a methods outcome you should consider the effects a method has on the whole program. Calling multiple times. Conversions of course must be pure. So should any method that is supposed to retrieve data, while it does not imply setting some state. Especially, this is true for an exposed API, where the caller is not familiar with the internals of your code. Callers rely on you following the principle of command-query-separation.
        /// Code contracts are one use case, which enforce you to manually annotate method with the PureAttribute, though they do not check validity of an annotation.
        /// Rules
        /// The enumeration Purity is a set of flags, where Pure is zero. Any method can be marked with multiple flags, which define different reasons why the method is not pure. Only methods of the current project are considered. Methods in framework or foreign modules are not analyzed.
        /// Allowed for Purity.Pure:
        /// - The method modifies an object it instantiates 
        /// - The method modifies its return value 
        /// - Call pure methods 
        /// - Call methods that modify instantiated objects 
        /// - Call methods that modify the return value
        /// Nothing of the following must be true:
        /// - Set a static field = Purity.ModifyCommon 
        /// - Call a method that sets a static field (applies recursively) = Purity.CallModifyCommon 
        /// - Modify the current objects state or the state of a member = Purity.ModifySelf 
        /// - Call a method of the current object that modifies the current object (applies recursively) = Purity.CallModifySelf 
        /// - Call a method of a member that is marked with Purity.ModifySelf or Purity.CallModifySelf = Purity.CallModifySelf 
        /// - Change the state of a parameter = Purity.ModifyParameter 
        /// - Call a method of a parameter that is marked with Purity.ModifyCommon, Purity.CallModifyCommon, Purity.ModifySelf or Purity.CallModifySelf = Purity.CallModifyParameter 
        /// - Call a method with a parameter that takes a parameter of the current method or the current object and that is marked with Purity.ModifyParameter or Purity.CallModifyParameter = Purity.CallModifyParameter
        /// When Methodenreinheit is called for a member other than a method then the result will be Purity.Undefined.
        /// Note: The principle method EvaluateMethodenreinheit determines the purity value for all methods in the current project. By default the value of MemberDescriptor.Purity is initialized with Undefined. Only after executing Methodenreinheit the property is set.
        /// </summary>
        /// <param name="model">model to evaluate</param>
        public static void EvaluateMethodenreinheit(IDependencyModel model)
        {
            var methodenreinheit = new Methodenreinheit(model);
            methodenreinheit.Evaluate();
        }
    }
}
